﻿namespace WinRT.Triggers
{
    #region Using directives

    using System;
    using System.Reflection;
    using System.Runtime.InteropServices.WindowsRuntime;

    using Windows.UI.Xaml;

    #endregion

    /// <summary>
    /// A trigger that needs to hook into an even on an associated object, which when fired will
    /// invoke the trigging process.
    /// </summary>
    public abstract class EventHookingTrigger : Trigger
    {
        #region Fields

        /// <summary>
        /// The hooked event.
        /// </summary>
        private WeakReference<EventHooker> hookedEvent;

        #endregion

        #region Methods

        /// <summary>
        /// Gets the object the event is exposed on.
        /// </summary>
        /// <param name="sourceObject">
        /// The source object to use - if this is null then the currently associated
        /// object will be used.
        /// </param>
        /// <returns>
        /// The event target.
        /// </returns>
        protected object GetEventTarget(object sourceObject)
        {
            return sourceObject == DependencyProperty.UnsetValue || sourceObject == null
                       ? this.AssociatedObject
                       : sourceObject;
        }

        /// <summary>
        /// Hooks a named event on the source object.
        /// </summary>
        /// <param name="eventName">
        /// The name of the event.
        /// </param>
        /// <param name="sourceObject">
        /// The source object that exposes the event - if this is null then the
        /// currently associated object will be used.
        /// </param>
        protected void HookEvent(string eventName, object sourceObject)
        {
            if (!string.IsNullOrEmpty(eventName))
            {
                var target = this.GetEventTarget(sourceObject);

                if (target != null)
                {
                    var eventHooker = new EventHooker(this);
                    eventHooker.HookEvent(target, eventName);

                    this.hookedEvent = new WeakReference<EventHooker>(eventHooker);
                }
            }
        }

        /// <summary>
        /// Unhooks the currently hooked event.
        /// </summary>
        /// <param name="sourceObject">
        /// The source object to use - if this is null then the currently associated
        /// object will be used.
        /// </param>
        protected void UnhookEvent(object sourceObject)
        {
            EventHooker currentEventHooker;

            var eventTarget = this.GetEventTarget(sourceObject);
            if (eventTarget != null && this.hookedEvent != null && this.hookedEvent.TryGetTarget(out currentEventHooker))
            {
                currentEventHooker.UnhookEvent(eventTarget);
            }

            this.hookedEvent = null;
        }

        #endregion

        /// <summary>
        /// A helper class capable of wrapping an arbitrary event handler on a <see cref="FrameworkElement"/>.
        /// </summary>
        private sealed class EventHooker
        {
            #region Fields

            /// <summary>
            /// The <see cref="EventTrigger"/> that owns this instance
            /// </summary>
            private readonly EventHookingTrigger binding;

            /// <summary>
            /// The current event information.
            /// </summary>
            private EventInfo eventInfo;

            /// <summary>
            /// The WinRT event registration token - used when the event is a WinRT event.
            /// </summary>
            private EventRegistrationToken? eventRegistrationToken;

            /// <summary>
            /// The handler attached to the event.
            /// </summary>
            private Delegate handler;

            #endregion

            #region Constructors and Destructors

            /// <summary>
            /// Initializes a new instance of the <see cref="EventHooker"/> class. 
            /// Initializes the instance of the class
            /// </summary>
            /// <param name="binding">
            /// The <see cref="EventTrigger"/> that owns this instance.
            /// </param>
            public EventHooker(EventHookingTrigger binding)
            {
                this.binding = binding;
            }

            #endregion

            #region Public Methods and Operators

            /// <summary>
            /// Adds a hooks into the associated object for the event with the given name.
            /// </summary>
            /// <param name="obj">
            /// The object to attach the event to.
            /// </param>
            /// <param name="eventName">
            /// The name of the event to hook into.
            /// </param>
            public void HookEvent(object obj, string eventName)
            {
                this.eventInfo = GetEventInfo(obj.GetType(), eventName);

                if (this.eventInfo != null)
                {
                    this.handler = this.GetEventHandler(this.eventInfo);

                    if (this.eventInfo.AddMethod.ReturnType == typeof(EventRegistrationToken))
                    {
                        // Add the event handler and keep a reference to the event registration token.
                        WindowsRuntimeMarshal.AddEventHandler(
                            e =>
                                {
                                    this.eventRegistrationToken = (EventRegistrationToken)this.eventInfo.AddMethod.Invoke(obj, new object[] { e });
                                    return this.eventRegistrationToken.Value;
                                }, 
                            e => this.eventInfo.RemoveMethod.Invoke(obj, new object[] { e }), 
                            this.handler);
                    }
                    else
                    {
                        this.eventInfo.AddMethod.Invoke(obj, new object[] { this.handler });
                    }
                }
            }

            /// <summary>
            /// Removes a hook from the associated object for the event with the given name.
            /// </summary>
            /// <param name="obj">
            /// The object to detach the event from.
            /// </param>
            public void UnhookEvent(object obj)
            {
                if (this.eventInfo != null && this.handler != null)
                {
                    if (this.eventInfo.AddMethod.ReturnType == typeof(EventRegistrationToken))
                    {
                        if (this.eventRegistrationToken != null)
                        {
                            this.eventInfo.RemoveMethod.Invoke(obj, new object[] { this.eventRegistrationToken });
                            this.eventRegistrationToken = null;
                        }
                    }
                    else
                    {
                        this.eventInfo.RemoveMethod.Invoke(obj, new object[] { this.handler });
                    }

                    this.handler = null;
                    this.eventInfo = null;
                }
            }

            #endregion

            #region Methods

            /// <summary>
            /// Gets <see cref="EventInfo"/> information about the specified event name of the provided type.
            /// </summary>
            /// <param name="type">
            /// The type that the event should be found on.
            /// </param>
            /// <param name="eventName">
            /// The name of the event to get the information for.
            /// </param>
            /// <returns>
            /// Instance of <see cref="EventInfo"/> representing the named event.
            /// </returns>
            private static EventInfo GetEventInfo(Type type, string eventName)
            {
                EventInfo eventInfo;

                // GetDeclaredEvent doesn't get event info for events that are declared higher up in the
                // type hierarchy, so keep working up the base types until we find it.
                do
                {
                    var typeInfo = type.GetTypeInfo();
                    eventInfo = typeInfo.GetDeclaredEvent(eventName);
                    type = typeInfo.BaseType;
                }
                while (eventInfo == null && type != null);

                return eventInfo;
            }

            /// <summary>
            /// Create an instance of a delegate able to handle the provided event
            /// </summary>
            /// <param name="eventInfo">
            /// Information about the event to create the delegate
            /// </param>
            /// <returns>
            /// The <see cref="Delegate"/>.
            /// </returns>
            private Delegate GetEventHandler(EventInfo eventInfo)
            {
                if (eventInfo == null)
                {
                    throw new ArgumentNullException("eventInfo");
                }

                if (eventInfo.EventHandlerType == null)
                {
                    throw new ArgumentException("eventInfo.EventHandlerType must be set");
                }

                var eventRaisedMethod = this.GetType().GetTypeInfo().GetDeclaredMethod("OnEventRaised");
                return eventRaisedMethod.CreateDelegate(eventInfo.EventHandlerType, this);
            }

            /// <summary>
            /// This method is hooked, via reflection, to a named event. It is responsible for 
            /// triggering the 
            /// </summary>
            /// <param name="sender">
            /// source of the event
            /// </param>
            /// <param name="e">
            /// arguments of the event
            /// </param>
            private void OnEventRaised(object sender, object e)
            {
                // ReSharper restore UnusedParameter.Local
                // ReSharper restore UnusedMember.Local
                this.binding.OnTriggered(e);
            }

            #endregion
        }
    }
}